<!DOCTYPE HTML>
<html>
<head>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
</head>
<body>
<script>

// If a constraint is set with applyConstraints, it should come back in
// getConstraints().
promise_test(() => {
  return navigator.mediaDevices.getUserMedia({video: {width: {exact: 800}}})
      .then(s => {
    var track = s.getVideoTracks()[0];
    var constraints = track.getConstraints();
    assert_equals(Object.keys(constraints).length, 1);
    assert_true(constraints.hasOwnProperty('width'));
    assert_equals(constraints.width.exact, 800);

    return track.applyConstraints({width: {exact: 640}})
        .then(() => {
      constraints = track.getConstraints();
      assert_equals(Object.keys(constraints).length, 1);
      assert_true(constraints.hasOwnProperty('width'));
      assert_equals(constraints.width.exact, 640);
    })
  });
}, 'applyConstraints() sets the value of a constraint set by getUserMedia()');

// The deviceId constraint must be rejected, since the source of a track cannot
// be changed with applyConstraints.
promise_test(test => {
  return promise_rejects_js(test,
                            OverconstrainedError,
                            navigator.mediaDevices.getUserMedia({audio: true})
      .then(s => {
    return s.getAudioTracks()[0].applyConstraints(
        { deviceId: {exact: 'mydevice-id'}});
  }));
}, 'Attempting to change the deviceId with applyConstraints() fails');

promise_test(() => {
  let track;
  return navigator.mediaDevices.getUserMedia({video: true})
      .then(s => {
    track = s.getVideoTracks()[0];
    return track.getSettings();
  }).then(settings => {
    return track.applyConstraints(
        { width: { exact: settings.width }, notKnownName: { exact: true }})
  }).then(() => {
    constraints = track.getConstraints();
    assert_equals(Object.keys(constraints).length, 1);
    assert_false(constraints.hasOwnProperty('notKnownName'));
  });
}, 'An unsupported constraint is ignored by applyConstraints()');

function constraintElementsEqual(a, b) {
  if (a === b)
    return true;
  if (!(a instanceof Object))
    return false;
  if (!(b instanceof Object))
    return false;
  if (Object.keys(a).length != Object.keys(b).length)
    return false;
  for (var p in a) {
    if (!a.hasOwnProperty(p))
      continue;  // Skip prototypes and such things.
    if (!b.hasOwnProperty(p))
      return false;
    if (a[p] instanceof Object && b[p] instanceof Object) {
      if (!constraintElementsEqual(a[p], b[p]))
         return false;
      continue;
    }
    if (a[p] !== b[p]) return false;  // Simple types.
  }
  return true;
}

promise_test(() => {
  // Construct a constraint set that covers constraints that make sense for
  // video.
  const complexConstraintSet = {
    width: { min: 30, max: 480 },
    height: { min: 30, max: 480, exact: 350 },
  };
  // These constraints are syntactically valid, but may cause rejection.
  // They are included in an "advanced" constraint.
  // The particular values chosen are picked to exercise multiple parser paths,
  // but the most important is that all legal names are represented.
  const ignorableConstraintSet = {
    frameRate: { ideal: 30.0 },
    facingMode: { ideal: "user" },
    aspectRatio: { ideal: 1.3333333, exact: 1.4444 },

    sampleRate: { ideal: 42, min: 31, max: 54 },
    sampleSize: 3,
    echoCancellation: { ideal: false, exact: true },
    autoGainControl: { ideal: false, exact: true },
    noiseSuppression: { ideal: false, exact: true },
    latency: 0.22,
    channelCount: 2,
    deviceId: { ideal: ["foo", "fooz"] },
    groupId: ["bar", "baz"]
  };
  let complexConstraints = complexConstraintSet;
  complexConstraints.advanced = [ ignorableConstraintSet ];

  return navigator.mediaDevices.getUserMedia({video: true})
      .then(s => {
    var track = s.getVideoTracks()[0];
    return track.applyConstraints(complexConstraints).then(() => {
      constraints = track.getConstraints();
      assert_true(constraintElementsEqual(constraints, complexConstraints),
        "Unexpected result: In: " +
        JSON.stringify(complexConstraints, null, 2) +
        " Out: " + JSON.stringify(constraints, null, 2));
    });
  });
}, 'All valid keys are returned for complex constraints');

// Syntax tests for constraints.
// These work by putting the constraints into an advanced constraint
// (so that they can be ignored), calling getUserMedia, and then
// inspecting the constraints.
// In advanced constraints, naked values mean "exact", and "exact" values
// are thus unwrapped, which is the opposite behavior from the "basic"
// constraint set (outside advanced).
function constraintSyntaxTestWithChange(name, constraints, expected_result) {
  promise_test(() => {
    return navigator.mediaDevices.getUserMedia(
        {'video': true})
        .then(s => {
      var track = s.getVideoTracks()[0];
      track.applyConstraints({ 'advanced': [ constraints ]}).then(() => {
      var constraints_out = track.getConstraints().advanced[0];
      assert_true(constraintElementsEqual(expected_result, constraints_out),
          "Unexpected result: Expected: " +
          JSON.stringify(expected_result, null, 2) +
          " Out: " + JSON.stringify(constraints_out, null, 2));
      })
    })
  }, name);
}

function constraintSyntaxTest(name, constraints) {
  constraintSyntaxTestWithChange(name, constraints, constraints);
}

constraintSyntaxTest('Simple integer', { height: 42 });
constraintSyntaxTest('Ideal integer', { height: { ideal: 42 }});
constraintSyntaxTest('Min/max integer', { height: { min: 42, max: 43 }});
constraintSyntaxTestWithChange('Exact unwrapped integer',
                               { height: { exact: 42 } }, { height: 42 });

constraintSyntaxTest('Simple double', { aspectRatio: 1.5 });
constraintSyntaxTest('Ideal double', { aspectRatio: { ideal: 1.5 }});
constraintSyntaxTest('Min/max double', { aspectRatio: { min: 1.5, max: 2.0 }});
constraintSyntaxTestWithChange(
    'Exact unwrapped double',
    { aspectRatio: { exact: 1.5 } }, { aspectRatio: 1.5 });

constraintSyntaxTest('Simple String', { facingMode: "user1" });
constraintSyntaxTest('Ideal String', { facingMode: { ideal: "user2" }});
constraintSyntaxTest('Multiple String in Brackets',
                     { facingMode: { ideal: ["user3", "left3"]}});
constraintSyntaxTest('Multiple Bracketed Naked String',
                     { facingMode: ["user4", "left4"] });
constraintSyntaxTestWithChange(
    'Single Bracketed string unwrapped',
    { 'facingMode': ["user5"]}, { facingMode: "user5" });
constraintSyntaxTest('Both Ideal and Exact string',
                     { facingMode: { ideal: "user6", exact: "left6" }});

constraintSyntaxTest('echoCancellation with simple boolean value', { echoCancellation: true });
constraintSyntaxTest('echoCancellation with ideal boolean value', { echoCancellation: { ideal: true }});
constraintSyntaxTestWithChange('echoCancellation with exact unwrapped boolean value',
    { echoCancellation: { exact: true } }, { echoCancellation: true });

constraintSyntaxTest('autoGainControl with simple boolean value', { autoGainControl: true });
constraintSyntaxTest('autoGainControl with ideal boolean value', { autoGainControl: { ideal: true }});
constraintSyntaxTestWithChange('autoGainControl with exact unwrapped boolean value',
    { autoGainControl: { exact: true } }, { autoGainControl: true });

constraintSyntaxTest('noiseSuppression with simple boolean value', { noiseSuppression: true });
constraintSyntaxTest('noiseSuppression with ideal boolean value', { noiseSuppression: { ideal: true }});
constraintSyntaxTestWithChange('noiseSuppression with exact unwrapped boolean value',
    { noiseSuppression: { exact: true } }, { noiseSuppression: true });

promise_test(async t => {
  let stream = await navigator.mediaDevices.getUserMedia(
    {video: {width: 639, height: 479}});
  let track = stream.getVideoTracks()[0];
  t.add_cleanup(()=>track.stop());
  let settings = track.getSettings();
  assert_equals(settings.width, 639);
  assert_equals(settings.height, 479);
  assert_equals(settings.resizeMode, "crop-and-scale");

  await track.applyConstraints({resizeMode: {exact: "none"}});
  settings = track.getSettings();
  assert_equals(settings.width, 640);
  assert_equals(settings.height, 480);
  assert_equals(settings.resizeMode, "none");

  try {
    await track.applyConstraints(
      {width: {exact: 639}, resizeMode: {exact: 'none'}});
    t.step(()=>assert_unreached('applyConstraints should not have succeeded'));
  } catch(e) {
    assert_equals(e.name, 'OverconstrainedError');
    assert_equals(e.constraint, 'width');
  }
}, "applyConstraints() supports resizeMode in getUserMedia() tracks");

function constraintSyntaxTestContradictingProperties(
    name, constraintName, constraints, contradictingConstraints) {
  promise_test(async t => {
    const stream = await navigator.mediaDevices.getUserMedia(constraints);
    const tracks = stream.getTracks();
    assert_equals(tracks.length, 1);

    const track = tracks[0];
    t.add_cleanup(()=>track.stop());
    try {
      await track.applyConstraints(contradictingConstraints);
    } catch(e) {
      assert_equals(e.name, 'OverconstrainedError');
      assert_equals(e.constraint, constraintName);
      return;
    }
    assert_unreached('applyConstraints should not have succeeded');
  }, name);
}

constraintSyntaxTestContradictingProperties(
    'Contradicting echoCancellation constraints', 'echoCancellation',
    {audio: {echoCancellation: {exact: true}}},
    {echoCancellation: {exact: false}});

constraintSyntaxTestContradictingProperties(
    'Contradicting noiseSuppression constraints', 'noiseSuppression',
    {audio: {noiseSuppression: {exact: true}}},
    {noiseSuppression: {exact: false}});

constraintSyntaxTestContradictingProperties(
    'Contradicting autoGainControl constraints', 'autoGainControl',
    {audio: {autoGainControl: {exact: true}}},
    {autoGainControl: {exact: false}});

</script>
</body>
</html>
